package com.zty.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zty.util.RequestUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author ZTY
 * @Description 切面日志（接口调用日志）
 * @Date 1:06 2023-12-23
 **/

@Aspect
@Slf4j
@Component
public class ApiLoggerAspect {
    private final HttpServletRequest request;
    private final ObjectMapper objectMapper;
    public ApiLoggerAspect(HttpServletRequest request,
                           ObjectMapper objectMapper) {
        this.request = request;
        this.objectMapper = objectMapper;
    }

    /**
     * 切入点为controller的每一个接口
     */
    @Pointcut("execution(* com.zty.controller..*Controller.*(..))")
    public void apiLog() {
    }

    /**
     *
     * @param point
     * ProceedingJoinPoint表示程序执行过程中的一个特定点，比如方法的调用。
     * 它包含了关于这个切点的信息，比如方法的名称、参数等。
     * 通过使用ProceedingJoinPoint，你可以在通知中访问和操作这些信息，甚至可以控制方法的执行。
     * @return
     * @throws Throwable
     */
    @Around("apiLog()")
    public Object around(ProceedingJoinPoint point) throws Throwable {

        LogModel logModel = new LogModel();
        long start = System.currentTimeMillis();

        //  result为返回的结果
        Object result = point.proceed();

        // 解析请求参数（注入ip地址，请求方式，链接，参数列表）
        parseRequest(point, logModel);

        // 将返回的结果注入
        logModel.setResponse(result);

        long end = System.currentTimeMillis();

        //  将当前时间戳和响应所消耗的时间注入
        logModel.setTimestamp(end)
                .setCost(end - start);

        //  若正常执行，则无需注入异常信息
        //  将对象转化为比较漂亮的json字符串，打印出来
        log.info(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(logModel));
        return result;
    }

    /**
     * 异常对象，抛出异常e后才注入
     * @param point
     * @param e
     * @throws JsonProcessingException
     */
    @AfterThrowing(value = "apiLog()", throwing = "e")
    public void afterThrowing(JoinPoint point, Throwable e) throws JsonProcessingException {

        long start = System.currentTimeMillis();

        //  将报错的堆栈信息取出
        StackTraceElement[] stackTrace = e.getStackTrace();

        //  将堆栈信息中的不好看的中括号给去掉
        String stackTracing = Arrays.toString(stackTrace).replace("[",
                "").replace("]", "");

        //  取出最顶部的堆栈信息（最终报错）
        StackTraceElement stackTraceElement = e.getStackTrace()[0];

        //  定义报错信息对象
        ExceptionInfo exceptionInfo = new ExceptionInfo();


        exceptionInfo.setMessage(e.getMessage())
                .setFilename(stackTraceElement.getFileName())
                .setClassName(stackTraceElement.getClassName())
                .setMethodName(stackTraceElement.getMethodName())
                .setLineNumber(stackTraceElement.getLineNumber())
                //  其他细节即将整个堆栈打印出来
                .setDetails(stackTracing);

        //  报错则不用打印返回的结果
        LogModel logModel = new LogModel();
        logModel.setException(exceptionInfo);
        parseRequest((ProceedingJoinPoint) point, logModel);

        //  将当前时间戳和响应所消耗的时间注入
        long end = System.currentTimeMillis();
        logModel.setTimestamp(end)
                .setCost(end - start);

        //  将对象转化为比较漂亮的json字符串，打印出来
        log.info(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(logModel));
    }


    /**
     * 将一个请求中的原信息解析出来
     * @param point
     * @param logModel
     */
    private void parseRequest(ProceedingJoinPoint point, LogModel logModel) {

        //  获取发出请求的ip地址
        String ip = RequestUtil.getIpAddr(request);

        //  获取请求方式
        String method = request.getMethod();

        //  获取请求链接
        StringBuffer path = request.getRequestURL();

        //  获得方法的参数
        Object[] args = point.getArgs();

        //  获得方法的签名
        CodeSignature signature = (CodeSignature) point.getSignature();

        //  获得方法的参数名列表
        String[] paramNames = signature.getParameterNames();

        //  <参数名，参数> map
        Map<String, Object> params = new HashMap<>(paramNames.length);
        for (int i = 0; i < paramNames.length; i++) {
            params.put(paramNames[i], args[i].toString());
        }

        logModel.setIp(ip)
                .setMethod(method)
                .setPath(path.toString())
                .setParams(params);
    }

    /**
     * 将环绕日志定义成一个对象
     */
    @Data
    @Accessors(chain = true)
    public static class LogModel {
        //  请求的客户端
        private String ip;
        //  请求的类型（get? post?）
        private String method;
        //  请求的是哪一个接口
        private String path;
        //  请求的参数
        private Map<String, Object> params;
        //  请求的返回值
        private Object response;
        //  请求出现的异常信息类
        private ExceptionInfo exception;
        //  请求的发生时间
        private Long timestamp;
        //  响应所耗费的时间
        private Long cost;
    }

    /**
     * 将请求的异常信息定义成一个对象
     */
    @Data
    @Accessors(chain = true)
    public static class ExceptionInfo {
        //  具体的报错信息
        private String message;
        //  报错的文件
        private String filename;
        //  报错的类名
        private String className;
        //  报错的方法名
        private String methodName;
        //  第几行报错
        private Integer lineNumber;
        //  补充的细节
        private Object details;
    }
}